/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.fxbank.netbeans.plugins.utils;

/**
 *
 * @author alexey
 */
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Обект, представляющий XML-конфигурацию
 * @author bukarev-aa
 */
public class XMLProps extends DefaultHandler implements Serializable {
    private final static Pattern XML_CONFIG_PROPERTY_REGEXP = Pattern.compile("\\$\\{\\s*([A-z0-9.-]++)\\s*}");

    private XMLProps() {
    }
    
    private XMLProps(XMLNode root) {
        this.root = root;
    }

    /**
     * Загрузка XML-файла конфигурации из файла
     * @param file файл, в котором находится XML-конфигурация
     * @return
     * @throws javax.xml.parsers.ParserConfigurationException
     * @throws org.xml.sax.SAXException
     * @throws java.io.IOException
     */
    public static XMLProps load(File file, Properties props) throws ParserConfigurationException, SAXException, IOException {
        return file == null ? new XMLProps() : load(new FileInputStream(file), props);
        }

    public static XMLProps load(File file) throws ParserConfigurationException, SAXException, IOException {
        return XMLProps.load(file, null);
    }
    /**
     * Загрузка XML-файла конфигурации из потока
     * @param is поток, в котором находится XML-файл
     * @return
     * @throws javax.xml.parsers.ParserConfigurationException
     * @throws org.xml.sax.SAXException
     * @throws java.io.IOException
     */
    public static XMLProps load(InputStream is, Properties properties) throws SAXException, IOException, ParserConfigurationException {
        XMLProps props = new XMLProps();
        if(is != null) {
            props.properties = properties;
            SAXParserFactory factory = SAXParserFactory.newInstance();
            // возможно как-то устанавливается через System.setProperty, но не нашел
             // This feature instructs a parser to perform namespace processing,
             // which causes namespace prefixes, namespace URIs,
             // and element local names to be available through the SAX namespace callbacks (startPrefixMapping()
             // and endPrefixMapping(), as well as certain parameters supplied to startElement() and endElement()).
            factory.setFeature("http://xml.org/sax/features/namespaces", true);
            SAXParser parser = factory.newSAXParser();
            parser.parse(is, props);
        }
        return props;
    }

    public static XMLProps load(InputStream is) throws SAXException, IOException, ParserConfigurationException {
        return XMLProps.load(is, null);
    }

    /**
     * Возврат поддерева XML с заданным корневым элеметом
     * @param node узел исходного дерева
     * @return XML-дерево
     */
    public static XMLProps getSubXML(XMLNode node) {
        XMLNode newRoot = new XMLNode();
        newRoot.addChild(node);
        return new XMLProps(newRoot);
    }
    
    /**
     * Получение XML-файла из строки
     * @param s строка, содержащая XML-файл
     * @return
     * @throws javax.xml.parsers.ParserConfigurationException
     * @throws org.xml.sax.SAXException
     * @throws java.io.IOException
     */
    public static XMLProps load(String s) throws ParserConfigurationException, SAXException, IOException {
        return load(s, null);
    }

    /**
     * Получение XML-файла из строки
     * @param s строка, содержащая XML-файл
     * @param properties
     * @return
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     */
    public static XMLProps load(String s, Properties properties) throws ParserConfigurationException, SAXException, IOException {
        return s == null ? new XMLProps() : load(new ByteArrayInputStream(s.getBytes("UTF-8")), properties);
    }

    private XMLNode root = new XMLNode();
    private List<XMLNode> lastNodes = new LinkedList<XMLNode>();

    public XMLNode getRootNode() {
        return root;
    }

    /**
     * Делает подстановку переменных в строке input, значения берутся из props.
     * Переменные задаются как ${parameter}, в имени параметра могут учавствовать
     * латинские буквы, цифры, точка, подчеркивание.
     * @param input исходная строка с параметрами
     * @param props параметры и их значения
     * @return исходная строка с подставленными параметрами
     * @exception IllegalArgumentException в случае если параметр содержищийся в строке не был найден в props
     */
//    public static String replaceProperties(String input, Properties props) {
//        //DOTO Здесь надо подумать как оптимизировать
//        Matcher m = XML_CONFIG_PROPERTY_REGEXP.matcher(input);
//
//        // здесь будет храниться измененная строка
//        StringBuilder builder = new StringBuilder(input);
//
//
//        int start = 0; // позиция с которой начинать поиск
//        int shift = 0; // сдвиг текста после вставки в него како-то значения,
//                       // нужен из того что длинна имени параметра может отличатся от длинны значения
//        while (m.find(start)) {
//            start = m.start(); // изменяем позицию с которой в следущий раз будет вестись поиск
//
//            // извлекаeм значение т.е. убираем ${ и }
//            String group = m.group(1);
//            // получаем значение и проверяем что оно было указано
//            String value = props.getProperty(group);
//            if (value == null) {
//                // DOTO Скорее всего лучше заменить на другое Runtime исключение
//                throw new IllegalArgumentException("Property " + group + " not found");
//            }
//
//            // делаем замену
//            builder = builder.replace(start + shift, m.end() + shift, value);
//
//            // именяем сдвиг
//            shift += value.length() - m.group().length();
//
//            // увеличиваем стартовую позицию, чтобы не найти элемент повторно
//            start++;
//        }
//
//        return builder.toString();
//    }

    
    @Override
    public void startDocument() throws SAXException {
        lastNodes.add(root);
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        String s = new String(ch, start, length);

        // т.к. SAX-парсер мог разорвать текущий текст на несколько вызовов
        // данного метода, склеиваем предыдущую часть с текущей
        String lastNodeText = lastNodes.get(lastNodes.size() - 1).getText();
        String newNodeText = (lastNodeText == null || lastNodeText.length() == 0) ? s : (lastNodeText + s);

        // подставляем значения в переменные
//        newNodeText = replaceProperties(newNodeText, properties);

        // сохраняем текст
        lastNodes.get(lastNodes.size() - 1).setTextWithoutTrimming(newNodeText);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        String lastNodeText = lastNodes.get(lastNodes.size() - 1).getText();
        lastNodes.get(lastNodes.size() - 1).setText(lastNodeText);  // тримим текст
        lastNodes.remove(lastNodes.size() - 1);
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        XMLNode last = lastNodes.get(lastNodes.size() - 1);
        XMLNode node = new XMLNode(qName);
        for (int i = 0; i < attributes.getLength(); i++) {
            String key = attributes.getLocalName(i);
            String value = attributes.getValue(i);
            // подставляем значения в переменные
//            value = replaceProperties(value, properties);
            node.addAttr(key, value);
        }
        last.addChild(node);
        lastNodes.add(node);
    }

    /**
     * Простейший обработчик XPath<br/>
     * Понимает формат /node1/node2 - Выдаёт текстовое содержимое узла &lt;node1&gt;&lt;node2&gt;<br/>
     * Также понимает атрибуты /node1/@attr - Выдаёт значение атрибута attr узла &lt;node1&gt;
     * @param path  шаблон в формате XPath
     * @return текстовое содержимое узла/атрибута
     */
    public String getXPath(String path) {
        return getXPath(root, path);
    }

    public String getXPath(XMLNode node, String path) {
        for (String s : CommonUtils.fastSplit(path, '/')) {
//            if (s.length() == 0) {
//                continue;
//            }
            if (s.startsWith("@")) {
                return node.getAttr(s.substring(1));
            } else {
                boolean found = false;
                if(node.getChilds() != null) {
                    for (XMLNode xmln : node.getChilds()) {
                        if (xmln.getName().equals(s)) {
                            found = true;
                            node = xmln;
                            break;
                        }
                    }
                }
                if (!found) {
                    return Constants.EMPTY_STRING;
                }
            }
        }
        return node.getText();
    }

    /**
     * Простейший обработчик XPath<br/>
     * Понимает формат /node1/node2 - Возвращает все узлы, соответсвующие шаблону<br/>
     * Можно использовать символ '*'
     * @param path шаблон в формате XPath
     * @return набор узлов, соответствующих шаблону
     */
    public List<XMLNode> getXPathNodes(String path) {
        return getXPathNodes(root, path);
    }

    /**
     * Аналогично getXPathNodes(String), но путь вычисляется от узла <code>fromNode</code>
     * @param fromNode узел, от которого вычисляется путь по шаблону XPath
     * @param path шаблон в формате XPath
     * @return набор узлов, соответствующих шаблону
     */
    public List<XMLNode> getXPathNodes(XMLNode fromNode, String path) {
        List<XMLNode> foundedPrev = new LinkedList<XMLNode>();
        List<XMLNode> founded = new LinkedList<XMLNode>();
        founded.add(fromNode);
        for (String s : CommonUtils.fastSplit(path, '/')) {
//            if (s.length() == 0) {
//                continue;
//            }
            if (founded.isEmpty()) {
                return EMPTY_NODES;
            } else {
                foundedPrev = founded;
                founded = new LinkedList<XMLNode>();
            }
            for (XMLNode node : foundedPrev) {
                if (node.getChilds() != null) {
                    for (XMLNode xmln : node.getChilds()) {
                        if (ANY_NODE.equals(s) || xmln.getName().equals(s)) {
                            founded.add(xmln);
                        }
                    }
                }
            }
        }
        return founded;
    }

    public void setProperties(Properties props) {
        this.properties = props;
    }

    public Properties getProperties() {
        return properties;
    }

    
    @Override
    public String toString() {
        return root == null ? NULL_STRING : root.toString();
    }
    
    private final static List<XMLNode> EMPTY_NODES = new LinkedList<XMLNode>();
    private final static String ANY_NODE = "*", NULL_STRING = "null";
    private Properties properties;
}
